/*
 * Copyright (C) 2009 Leandro Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.metalframework.persistence.repository.collection.jpa;

import java.util.*;

import org.apache.commons.lang.math.*;
import org.metalframework.persistence.engine.*;

/**
 * JPA-based persistent collection strategy supporting extent with
 * {@link org.metalframework.persistence.annotation.FetchStrategy#BUFFERED}.
 * 
 * @param <T> Type of the entity whose strategy manipulates the extent.
 * 
 * @since 1.0
 * @see org.metalframework.persistence.annotation.FetchStrategy#BUFFERED
 * @see org.metalframework.persistence.repository.collection.PersistentCollectionStrategy
 * @see org.metalframework.persistence.repository.collection.jpa.AbstractJpaPersistentCollectionStrategy
 * @see org.metalframework.persistence.repository.collection.jpa.JpaPersistentCollectionStrategyFactory
 */
public class FetchBufferedJpaPersistentCollectionStrategy<T> extends AbstractJpaPersistentCollectionStrategy<T> {

	private static final long serialVersionUID = 4318374058614557250L;

	/**
	 * Number of entities in the extent.
	 */
	private int size;
	
	/**
	 * Number of entities to retrieve when querying the repository.
	 */
	private int fetchSize;
	
	/**
	 * Entity buffer.
	 */
	private Map<Integer, T> buffer;
	
	/**
	 * Entities that has been modified since the last flush.
	 */
	private Map<Integer, T> modifiedEntities;
	
	/**
	 * Positions of the entities removed since the last flush.
	 */
	private List<Integer> removedIndexes;
	
	/**
	 * Creates the strategy with the specified parameters.
	 * 
	 * @param entityType Type of the entity whose strategy manipulates the extent.
	 * @param jpaOperations Helper to execute JPA operations.
	 */
	public FetchBufferedJpaPersistentCollectionStrategy(Class<T> entityType, JapaEngine japaEngine, int fetchSize) {
		super(entityType, japaEngine);
		
		if (fetchSize <= 0) {
			throw new IllegalArgumentException("Fetch size must be greater than zero");
		}
		this.fetchSize = fetchSize;
		this.buffer = new TreeMap<Integer, T>();
		this.modifiedEntities = new HashMap<Integer, T>();
		this.removedIndexes = new ArrayList<Integer>();

		this.size = count();
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void doAdd(int index, T entity) {
		verifyBounds(index);

		// Keep entity to return it when buffer moves to its position
		T aux = entity;
		int counter = index;
		do {
			aux = modifiedEntities.put(counter++, aux);
		} while (aux != null);

		// Add to buffer
		aux = entity;
		counter = index;
		do {
			aux = buffer.put(counter++, aux);
		} while (aux != null);
		
		// Keep buffer size less than or equals to fetchSize
		if (buffer.size() > fetchSize) {
			buffer.remove(buffer.keySet().iterator().next());
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected T doGet(int index) {
		verifyBounds(index);
		
		if (!buffer.containsKey(index)) {
			fillBuffer(index);
		}
		
		return buffer.get(index);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected T doSet(int index, T entity) {
		verifyBounds(index);
		
		modifiedEntities.put(index, entity);
		if (!buffer.containsKey(index)) {
			fillBuffer(index);
		}
		
		return buffer.put(index, entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected T doRemove(int index) {
		verifyBounds(index);
		
		if (!buffer.containsKey(index)) {
			fillBuffer(index);
		}
		T entity = buffer.remove(index);
		removedIndexes.add(index);
		size--;
		
		return entity;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected boolean doRemove(T entity) {
		return doBufferOrSequentialRemove(buffer, entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected boolean doContains(T entity) {
		return doBufferOrSequentialContains(buffer, entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected int doSize() {
		return size;
	}

	/**
	 * Clears the entity buffer and updates extent size.
	 */
	@Override
	protected void flushed() {
		buffer.clear();
		modifiedEntities.clear();
		size = count();
	}

	/**
	 * Retrieves entities starting from specified position to fill the entity buffer.
	 * 
	 * @param startPosition Position of the first entity to retrieve.
	 */
	protected void fillBuffer(int startPosition) {
		int actualSize = fetchSize;
		Range range = new IntRange(startPosition, startPosition + fetchSize);
		for (Integer i : removedIndexes) {
			if (range.containsInteger(i)) {
				actualSize++;
			}
		}
		List<T> entities = select(startPosition, actualSize);
		
		buffer.clear();
		for (int i = 0; i < entities.size();) {
			int index = i + startPosition;
			int indexFrequency = Collections.frequency(removedIndexes, Integer.valueOf(index));

			if (indexFrequency > 0) {
				// The current index and the next ones must be ignored because the were removed
				i += indexFrequency;
			} else {
				// Verify if entity has been modified before
				if (modifiedEntities.containsKey(index)) {
					buffer.put(index, modifiedEntities.get(index));
				} else {
					buffer.put(index, entities.get(i));
				}
				i++;
			}
		}
	}
}
